1 module widgets; 2 3 import models; 4 5 import jupyter.wire.kernel; 6 import jupyter.wire.message; 7 import std.json : JSONValue, parseJSON; 8 import std.meta; 9 10 interface IWidget { 11 void close(scope IoPubMessageSender sender) @safe; 12 void update(scope IoPubMessageSender sender, JSONValue newState) @safe; // process change from the backend (will send change to frontend as well) 13 void onUpdate(in JSONValue newState, in JSONValue buffer_paths) @safe; // process change from the frontend 14 void onCustomMessage(scope IoPubMessageSender sender, in JSONValue content) @safe; 15 void onRequestState(scope IoPubMessageSender sender) @safe; 16 void onRemove(scope IoPubMessageSender sender, in JSONValue data) @safe; 17 void display(scope IoPubMessageSender sender) @safe; 18 string getCommId() @safe; 19 } 20 21 enum WidgetProtocolMetadata = parseJSON(`{"version":"2.0.0"}`); 22 23 // Each Widget is templatized on a model. A model may contain reference to other models, which each need an instantiated widget. 24 // This helper struct initializes all referenced widgets. 25 // e.g. many models reference a LayoutModel or a StyleModel 26 struct ReferenceWidgets(T) { 27 alias makeWidget(T) = Widget!T; 28 alias Models = getReferenceModels!T.Types; 29 alias Names = getReferenceModels!T.Names; 30 alias Widgets = staticMap!(makeWidget, Models); 31 static foreach(idx, W; Widgets) { 32 mixin("W "~Names[idx]~";"); 33 } 34 void initialize(ref T state, scope IoPubMessageSender sender) @safe { 35 static foreach (idx, Widget; Widgets) {{ 36 auto widget = new Widget(sender); 37 mixin(Names[idx]~" = widget;"); 38 __traits(getMember, state, Names[idx]) = widget.commId.makeReference(); 39 }} 40 } 41 } 42 43 class Widget(T) : IWidget { 44 private string commId; 45 private ReferenceWidgets!T referenceWidgets; 46 T state; 47 48 this(scope IoPubMessageSender sender) @safe { 49 import std.uuid: randomUUID; 50 this(sender, randomUUID.toString); 51 } 52 53 this(scope IoPubMessageSender sender, string commId) @safe { 54 this(sender, commId, T()); 55 sender(commOpenMessage(commId, "jupyter.widget", this.state.serialize(), WidgetProtocolMetadata)); 56 } 57 58 this(scope IoPubMessageSender sender, string commId, T state) @safe { 59 this.commId = commId; 60 this.state = state; 61 referenceWidgets.initialize(this.state, sender); 62 } 63 64 override void close(scope IoPubMessageSender sender) @safe { 65 sender(commCloseMessage(commId)); 66 } 67 68 override void update(scope IoPubMessageSender sender, JSONValue newState) @safe { 69 this.state.update(newState, JSONValue()); 70 JSONValue data; 71 data["state"] = newState; 72 sender(commMessage(commId, data, WidgetProtocolMetadata)); 73 } 74 75 override void onUpdate(in JSONValue newState, in JSONValue buffer_paths) @safe { 76 this.state.update(newState, buffer_paths); 77 } 78 79 override void onRequestState(scope IoPubMessageSender sender) @safe { 80 auto data = this.state.serialize(); 81 data["method"] = "update"; 82 sender(commMessage(commId, data, WidgetProtocolMetadata)); 83 } 84 85 override void onCustomMessage(scope IoPubMessageSender sender, in JSONValue content) @safe { 86 } 87 88 override void onRemove(scope IoPubMessageSender sender, in JSONValue data) @safe { 89 } 90 91 override void display(scope IoPubMessageSender sender) @safe { 92 JSONValue widgetView; 93 widgetView["model_id"] = commId; 94 widgetView["version_major"] = 2; 95 widgetView["version_minor"] = 0; 96 JSONValue data; 97 data["application/vnd.jupyter.widget-view+json"] = widgetView; 98 sender(displayDataMessage(data)); 99 } 100 101 override string getCommId() @safe { 102 return commId; 103 } 104 } 105 106 @safe unittest { 107 import models : FloatSliderModel; 108 import std.algorithm : startsWith; 109 auto w = new Widget!FloatSliderModel((Message){}); 110 assert(w.state.style.startsWith("IPY_MODEL_")); 111 assert(w.state.layout.startsWith("IPY_MODEL_")); 112 } 113 114 IWidget constructWidget(in string commId, in JSONValue data, scope IoPubMessageSender sender) @safe { 115 import models : AllModels; 116 import std.format : format; 117 118 const modelModule = data["_model_module"].str; 119 const modelName = data["_model_name"].str; 120 const viewModule = data["_view_module"].str; 121 const viewName = data["_view_name"].str; 122 123 static foreach(Model; AllModels) { 124 if (Model._model_module == modelModule && 125 Model._model_name == modelName && 126 Model._view_module == viewModule && 127 Model._view_name == viewName) { 128 Model model; 129 model.update(data, JSONValue()); 130 return new Widget!(Model)(sender, commId, model); 131 } 132 } 133 134 throw new Exception("Cannot construct widget module %s:%s with view %s:%s".format(modelModule, modelName, viewModule, viewName)); 135 } 136 137 @safe unittest { 138 import std.json : parseJSON; 139 bool isCalled = false; 140 141 auto widget = constructWidget("abcd", parseJSON(`{"_model_module":"@jupyter-widgets/controls","_model_name":"FloatSliderModel","_view_module":"@jupyter-widgets/controls","_view_name":"FloatSliderView","max":250.0}`), (Message msg){}); 142 143 assert(widget !is null); 144 widget.onRequestState((Message msg){ 145 if (msg.content["comm_id"].str == "abcd") { 146 isCalled = true; 147 assert(msg.content["data"]["state"]["max"].floating == 250.0); 148 } 149 }); 150 assert(isCalled == true); 151 } 152 153 @safe unittest { 154 import std.json : parseJSON; 155 bool isCalled = false; 156 157 auto widget = constructWidget("abcd", parseJSON(`{"_model_module":"@jupyter-widgets/controls","_model_name":"FloatSliderModel","_view_module":"@jupyter-widgets/controls","_view_name":"FloatSliderView","max":250.0}`), (Message msg){}); 158 159 assert(widget !is null); 160 widget.update((Message msg){ 161 if (msg.content["comm_id"].str == "abcd") { 162 isCalled = true; 163 assert(msg.content["data"]["state"]["max"].floating == 260.0); 164 } 165 }, parseJSON(`{"max": 260.0}`)); 166 assert(isCalled == true); 167 } 168